// ============================================ // CONFIGURAÇÃO GERAL // ============================================ var ID_PLANILHA = '1u1OESExWB_noM0YUCIi6XhnzmshpeFtxMzCjbFSNNi4'; var ID_FORMULARIO = '17GvJnG3-V578Gv69KRLPvSqBMxJefuCOIx2P65E9qFU'; var IDPASTAFOTOS = '1cQbwXZb4UPfRW2k3Ltjvmp_vItBmDinFraw4T-h0s_h0PuAqz0ATl710-jkV06LBkdsIF1wI'; var NOME_ABA = 'CADASTRO'; const FORM_ID = ID_FORMULARIO; const COLUNA_TAG = 3; const TITULO_PERGUNTA_TAG = 'Código da TAG'; var LINK_EDICAO = 'https://apptagemg.resinadosdf.com.br/gerenciar'; var LINK_PUBLICO_BASE = 'https://apptagemg.resinadosdf.com.br/pagpublica.html?tag='; // ============================================ // MAPA DE COLUNAS DA ABA CADASTRO (0-based) // ============================================ var IDX = { timestamp : 0, email : 1, tag : 2, nome : 3, telefone : 4, dataNasc : 5, tipoSang : 6, doadorOrgaos : 7, meds : 8, doencas : 9, alergias : 10, plano : 11, sus : 12, cont1Nome : 13, cont1Tel : 14, cont2Nome : 15, cont2Tel : 16, obs : 17, foto : 18, apelido_tag : 19, categoria_tag : 20, status_tag : 21, pet_nome : 22, pet_especie : 23, pet_raca : 24, pet_pelagem : 25, pet_sexo : 26, pet_castrado : 27, pet_nascimento : 28, pet_vacinas : 29, pet_saude : 30, pet_meds : 31, pet_procedimentos: 32, pet_vet_nome : 33, pet_vet_tel : 34, pet_vet_endereco : 35, pet_localizacao : 36, pet_exames : 37, pet_obs : 38, tutor_cont1_nome : 39, tutor_cont1_tel : 40, tutor_cont2_nome : 41, tutor_cont2_tel : 42, pet_vet_lat : 43, pet_vet_lng : 44 }; // ============================================ // AUXILIARES // ============================================ function getSpreadsheet() { return SpreadsheetApp.openById(ID_PLANILHA); } function getCadastroSheet() { var ss = getSpreadsheet(); var sheet = ss.getSheetByName(NOME_ABA); if (!sheet) throw new Error('Aba não encontrada: ' + NOME_ABA); return sheet; } function normalizeStr(v) { return String(v || '').trim(); } function normalizeEmail(v) { return normalizeStr(v).toLowerCase(); } function normalizeTag(v) { return normalizeStr(v).replace(/^'+/, ''); } function jsonResponse(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } function escapeHtml(text) { text = String(text || ''); return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatDateToISO(dateValue) { if (!(dateValue instanceof Date)) return String(dateValue || ''); return dateValue.getFullYear() + '-' + String(dateValue.getMonth() + 1).padStart(2, '0') + '-' + String(dateValue.getDate()).padStart(2, '0'); } function formatDateToBR(dateValue) { if (!(dateValue instanceof Date)) return String(dateValue || ''); return String(dateValue.getDate()).padStart(2, '0') + '/' + String(dateValue.getMonth() + 1).padStart(2, '0') + '/' + dateValue.getFullYear(); } function extrairIdDrive(url) { url = String(url || ''); var m1 = url.match(/[?&]id=([a-zA-Z0-9_-]+)/); if (m1 && m1[1]) return m1[1]; var m2 = url.match(/\/d\/([a-zA-Z0-9_-]+)/); if (m2 && m2[1]) return m2[1]; return ''; } function thumbDrive(url, size) { var id = extrairIdDrive(url); if (!id) return String(url || ''); return 'https://drive.google.com/thumbnail?id=' + id + '&sz=' + (size || 's600'); } function gerarCodigoVerificacao() { var num = Math.floor(Math.random() * 1000000); return ('000000' + num).slice(-6); } function montarChaveCodigoConta(email) { return 'OTP_CONTA_' + normalizeEmail(email); } function montarChaveCodigoTag(email, tag) { return 'OTP_TAG_' + normalizeEmail(email) + '_' + normalizeTag(tag); } function normalizarCategoria(valor) { var v = normalizeStr(valor).toLowerCase(); if (v === 'petmacho') return 'pet_macho'; if (v === 'petfemea') return 'pet_femea'; if (v === 'pet_femea') return 'pet_femea'; if (v === 'pet_macho') return 'pet_macho'; return 'emergencial'; } function normalizarStatus(valor) { var v = normalizeStr(valor).toUpperCase(); if (v === 'PERDIDA') return 'PERDIDA'; if (v === 'ARQUIVADA') return 'ARQUIVADA'; return 'ATIVA'; } function categoriaEhPet(cat) { cat = normalizarCategoria(cat); return cat === 'pet_macho' || cat === 'pet_femea'; } function garantirCabecalhoCadastro() { var sheet = getCadastroSheet(); var headers = [ 'timestamp','email','tag','nome','telefone','dataNasc','tipoSang','doadorOrgaos','meds','doencas', 'alergias','plano','sus','cont1Nome','cont1Tel','cont2Nome','cont2Tel','obs','foto', 'apelido_tag','categoria_tag','status_tag', 'pet_nome','pet_especie','pet_raca','pet_pelagem','pet_sexo','pet_castrado','pet_nascimento','pet_vacinas', 'pet_saude','pet_meds','pet_procedimentos','pet_vet_nome','pet_vet_tel','pet_vet_endereco','pet_localizacao', 'pet_exames','pet_obs','tutor_cont1_nome','tutor_cont1_tel','tutor_cont2_nome','tutor_cont2_tel', 'pet_vet_lat','pet_vet_lng' ]; var range = sheet.getRange(1, 1, 1, headers.length); var atual = range.getValues()[0]; var precisa = false; for (var i = 0; i < headers.length; i++) { if (String(atual[i] || '') !== headers[i]) { precisa = true; break; } } if (precisa) { range.setValues([headers]); sheet.setFrozenRows(1); } } function uploadFotoParaDrive(base64Data, tag, nome) { var token = ScriptApp.getOAuthToken(); var mimeType = 'image/jpeg'; var limpo = String(base64Data || ''); if (limpo.indexOf('base64,') > -1) limpo = limpo.split('base64,')[1]; var metadata = { name: normalizeTag(tag) + '_' + String(nome || '').replace(/[^a-zA-Z0-9]/g, '_').substring(0, 20) + '.jpg', parents: [IDPASTAFOTOS] }; var boundary = '-------TAG314159265'; var body = '--' + boundary + '\r\n' + 'Content-Type: application/json; charset=UTF-8\r\n\r\n' + JSON.stringify(metadata) + '\r\n' + '--' + boundary + '\r\n' + 'Content-Type: ' + mimeType + '\r\n' + 'Content-Transfer-Encoding: base64\r\n\r\n' + limpo + '\r\n' + '--' + boundary + '--'; var response = UrlFetchApp.fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id', { method: 'POST', contentType: 'multipart/related; boundary="' + boundary + '"', headers: { Authorization: 'Bearer ' + token }, payload: body, muteHttpExceptions: true }); var result = JSON.parse(response.getContentText()); if (!result.id) throw new Error('Upload falhou: ' + response.getContentText()); UrlFetchApp.fetch('https://www.googleapis.com/drive/v3/files/' + result.id + '/permissions', { method: 'POST', contentType: 'application/json', headers: { Authorization: 'Bearer ' + token }, payload: JSON.stringify({ role: 'reader', type: 'anyone' }), muteHttpExceptions: true }); return 'https://drive.google.com/thumbnail?id=' + result.id + '&sz=s600'; } // ============================================ // ROTAS // ============================================ function doGet(e) { var params = (e && e.parameter) ? e.parameter : {}; var action = params.action; var gerenciar = params.gerenciar; var tag = params.tag; var index = params.index; if (action === 'buscar') return buscarTag(e); if (index === '1') { return HtmlService.createHtmlOutputFromFile('index') .setTitle('TAG Emergencial') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } if (gerenciar === '1') { return HtmlService.createHtmlOutputFromFile('GerenciarTAG') .setTitle('Gerenciar TAG - TAG Emergencial') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } if (tag) { return HtmlService.createHtmlOutputFromFile('pagpublica') .setTitle('TAG ' + tag + ' - TAG Emergencial') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } return HtmlService.createHtmlOutputFromFile('pagpublica') .setTitle('TAG Emergencial') .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) .addMetaTag('viewport', 'width=device-width, initial-scale=1'); } function doPost(e) { try { garantirCabecalhoCadastro(); var body = JSON.parse((e && e.postData && e.postData.contents) ? e.postData.contents : '{}'); var action = body.action; if (action === 'verificarTagExistente') return jsonResponse({ existe: verificarTagExistente(body.tag) }); if (action === 'receberCadastroWebApp') return jsonResponse(receberCadastroWebApp(body)); if (action === 'validarAcesso') return jsonResponse(validarAcesso(body.email, body.tag)); if (action === 'validarCodigo') return jsonResponse(validarCodigo(body.email, body.tag, body.codigoDigitado)); if (action === 'buscarDadosTAG') return jsonResponse(buscarDadosTAG(body.email, body.tag)); if (action === 'atualizarDadosTAG') return jsonResponse(atualizarDadosTAG(body)); if (action === 'enviarCodigoAcessoConta') return jsonResponse(enviarCodigoAcessoConta(body.email)); if (action === 'validarCodigoAcessoConta') return jsonResponse(validarCodigoAcessoConta(body.email, body.codigoDigitado)); if (action === 'listarTagsPorEmail') return jsonResponse(listarTagsPorEmail(body.email)); if (action === 'buscarDadosTagPorLinha') return jsonResponse(buscarDadosTagPorLinha(body.email, body.numeroLinha)); if (action === 'atualizarMetaTag') return jsonResponse(atualizarMetaTag(body)); if (action === 'uploadFoto') { var url = uploadFotoParaDrive(body.fotoBase64, body.tag, body.nome); return jsonResponse({ sucesso: true, url: url }); } return jsonResponse({ erro: 'Ação não reconhecida: ' + action }); } catch (err) { return jsonResponse({ erro: 'Erro interno: ' + err.toString() }); } } // ============================================ // CONSULTA PÚBLICA // ============================================ function buscarTag(e) { try { var tag = normalizeTag(e && e.parameter && e.parameter.tag ? e.parameter.tag : ''); if (!tag) { return ContentService.createTextOutput(JSON.stringify({ erro: 'TAG não informada.' })) .setMimeType(ContentService.MimeType.JSON); } var sheet = getCadastroSheet(); var dados = sheet.getDataRange().getValues(); for (var i = 1; i < dados.length; i++) { if (normalizeTag(dados[i][IDX.tag]) !== tag) continue; var linha = dados[i]; var fotoThumb = thumbDrive(linha[IDX.foto], 's400'); var resultado = { tag: String(linha[IDX.tag] || ''), categoria: normalizarCategoria(linha[IDX.categoria_tag] || 'emergencial'), status: normalizarStatus(linha[IDX.status_tag] || 'ATIVA'), apelido: String(linha[IDX.apelido_tag] || '').trim(), nome: String(linha[IDX.nome] || ''), cpf: String(linha[IDX.telefone] || ''), nascimento: formatDateToBR(linha[IDX.dataNasc]), sangue: String(linha[IDX.tipoSang] || ''), doadorOrgaos: String(linha[IDX.doadorOrgaos] || ''), meds: String(linha[IDX.meds] || ''), doencas: String(linha[IDX.doencas] || ''), alergias: String(linha[IDX.alergias] || ''), plano: String(linha[IDX.plano] || ''), planoNum: String(linha[IDX.sus] || ''), cont1Nome: String(linha[IDX.cont1Nome] || ''), cont1Tel: String(linha[IDX.cont1Tel] || ''), cont2Nome: String(linha[IDX.cont2Nome] || ''), cont2Tel: String(linha[IDX.cont2Tel] || ''), obs: String(linha[IDX.obs] || ''), foto: fotoThumb, fotoLink: fotoThumb, petnome: String(linha[IDX.pet_nome] || ''), petespecie: String(linha[IDX.pet_especie] || ''), petraca: String(linha[IDX.pet_raca] || ''), petpelagem: String(linha[IDX.pet_pelagem] || ''), petsexo: String(linha[IDX.pet_sexo] || ''), petcastrado: String(linha[IDX.pet_castrado] || ''), petnascimento: formatDateToISO(linha[IDX.pet_nascimento]), petvacinas: String(linha[IDX.pet_vacinas] || ''), petsaude: String(linha[IDX.pet_saude] || ''), petmeds: String(linha[IDX.pet_meds] || ''), petprocedimentos: String(linha[IDX.pet_procedimentos] || ''), petvetnome: String(linha[IDX.pet_vet_nome] || ''), petvettel: String(linha[IDX.pet_vet_tel] || ''), petvetendereco: String(linha[IDX.pet_vet_endereco] || ''), petlocalizacao: String(linha[IDX.pet_localizacao] || ''), petexames: String(linha[IDX.pet_exames] || ''), petobs: String(linha[IDX.pet_obs] || ''), tutorcont1nome: String(linha[IDX.tutor_cont1_nome] || ''), tutorcont1tel: String(linha[IDX.tutor_cont1_tel] || ''), tutorcont2nome: String(linha[IDX.tutor_cont2_nome] || ''), tutorcont2tel: String(linha[IDX.tutor_cont2_tel] || ''), petvetlat: String(linha[IDX.pet_vet_lat] || ''), petvetlng: String(linha[IDX.pet_vet_lng] || '') }; return ContentService.createTextOutput(JSON.stringify(resultado)) .setMimeType(ContentService.MimeType.JSON); } return ContentService.createTextOutput(JSON.stringify({ erro: 'TAG não encontrada.' })) .setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService.createTextOutput(JSON.stringify({ erro: err.toString() })) .setMimeType(ContentService.MimeType.JSON); } } // ============================================ // CADASTRO INICIAL // ============================================ function verificarTagExistente(tag) { try { var sheet = getCadastroSheet(); var dados = sheet.getDataRange().getValues(); tag = normalizeTag(tag); for (var i = 1; i < dados.length; i++) { if (normalizeTag(dados[i][IDX.tag]) === tag) return true; } return false; } catch (e) { return false; } } function receberCadastroWebApp(dados) { try { garantirCabecalhoCadastro(); if (!dados.email || !dados.tag || !dados.nome) { return { sucesso: false, erro: 'Incompleto.' }; } if (verificarTagExistente(dados.tag)) { return { sucesso: false, erro: 'TAG em uso.' }; } var linkFoto = ''; if (dados.fotoLink && String(dados.fotoLink).startsWith('data:image')) { linkFoto = uploadFotoParaDrive(dados.fotoLink, dados.tag, dados.nome); } else if (dados.fotoLink && String(dados.fotoLink).startsWith('https://')) { linkFoto = dados.fotoLink; } var sheet = getCadastroSheet(); var row = new Array(IDX.pet_vet_lng + 1).fill(''); row[IDX.timestamp] = new Date(); row[IDX.email] = dados.email || ''; row[IDX.tag] = dados.tag || ''; row[IDX.nome] = dados.nome || ''; row[IDX.telefone] = dados.telefone || ''; row[IDX.dataNasc] = dados.dataNasc || ''; row[IDX.tipoSang] = dados.tipoSang || ''; row[IDX.doadorOrgaos] = dados.doadorOrgaos || ''; row[IDX.meds] = dados.meds || ''; row[IDX.doencas] = dados.doencas || ''; row[IDX.alergias] = dados.alergias || ''; row[IDX.plano] = dados.plano || ''; row[IDX.sus] = dados.sus || ''; row[IDX.cont1Nome] = dados.cont1Nome || ''; row[IDX.cont1Tel] = dados.cont1Tel || ''; row[IDX.cont2Nome] = dados.cont2Nome || ''; row[IDX.cont2Tel] = dados.cont2Tel || ''; row[IDX.obs] = dados.obs || ''; row[IDX.foto] = linkFoto || ''; row[IDX.apelido_tag] = dados.apelido || ''; row[IDX.categoria_tag] = normalizarCategoria(dados.categoria || 'emergencial'); row[IDX.status_tag] = normalizarStatus(dados.status || 'ATIVA'); sheet.appendRow(row); enviarEmailCadastro(dados); return { sucesso: true }; } catch (e) { return { sucesso: false, erro: e.toString() }; } } function enviarEmailCadastro(dados) { var linkPublico = LINK_PUBLICO_BASE + encodeURIComponent(dados.tag || ''); var assunto = '✅ TAG ' + (dados.tag || '') + ' - Cadastro Confirmado | ResinadosDF'; var corpo = 'Olá ' + (dados.nome || '') + ',\n\n' + 'Seu cadastro da TAG ' + (dados.tag || '') + ' foi recebido com sucesso!\n\n' + 'Gerenciar seus dados: ' + LINK_EDICAO + '\n' + 'Link público da sua TAG: ' + linkPublico + '\n\n' + 'Dicas importantes:\n' + '- Guarde este e-mail com o código da sua TAG: ' + (dados.tag || '') + '\n' + '- Mantenha seus dados sempre atualizados\n' + '- Em caso de emergência, o socorrista escaneará o QR Code\n\n' + 'Dúvidas? Responda este e-mail.\n\n' + 'Atenciosamente,\n' + 'Equipe ResinadosDF\n' + 'TAG Emergencial - Sua segurança em primeiro lugar'; var corpoHTML = "
Olá " + escapeHtml(dados.nome) + ",
" + "✅ Seu cadastro da TAG Emergencial " + escapeHtml(dados.tag) + " foi recebido com sucesso!
" + "📝 GERENCIAR SEUS DADOS
" + "Você pode editar suas informações, atualizar contatos de emergência ou alterar seus dados médicos a qualquer momento.
" + "Basta informar seu e-mail e o código da TAG para acessar.
" + "🔗 LINK PÚBLICO DA SUA TAG
" + "Este é o link que será aberto quando alguém escanear seu QR Code:
" + "💡 DICAS IMPORTANTES
" + "• Guarde este e-mail com o código da sua TAG: " + escapeHtml(dados.tag) + "
" + "• Mantenha seus dados sempre atualizados
" + "• Em caso de emergência, o socorrista escaneará o QR Code
" + "Dúvidas? Responda este e-mail.
" + "Atenciosamente,
" + "Equipe ResinadosDF
TAG Emergencial - Sua segurança em primeiro lugar
Olá,
' + 'Você solicitou acesso à sua TAG.
' + 'Código da TAG: ' + escapeHtml(tag) + '
' + '' + escapeHtml(codigo) + '
' + 'Este código é válido por 10 minutos.
' + 'Olá, ' + escapeHtml(nome) + '
' + 'Seu código de acesso ao painel é:
' + '' + escapeHtml(codigo) + '
' + 'Válido por 10 minutos.
' + '